package com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.xtree;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.db4o.odbgen.OdbgenException;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.DataType;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.OdbgenTypeConflictException;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.OdbgenTypeNotFoundException;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TSClass;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TSEnum;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TSField;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TypeDeclaration;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TypedXmlSchema;

/**
 * Holds a tree of classes, fields.
 * For example, consider two classes:
 * class User{
 *     Person person;
 *     String username;
 *     List<Email> emails;
 * }
 * class Person{
 *     String firstName;
 *     String lastName;
 *     User parentUser;
 * }
 * class Email{
 *     String email;
 * }
 * The tree will look like this:
 * -User
 *     -username
 *     -person
 *         -Person
 *             -firstName
 *             -lastName
 *             -parentUser
 *                 -User
 *                     -username
 *                     -person
 *                         -Person
 *                             -firstName
 *                             -lastName
 *                             -parentUser
 *                                 -We stop after a allowed depth level
 *                     -emails
 *                         -Email
 *                             -email
 *     -emails
 *         -Email
 *             -email
 * -Person
 *     -firstName
 *     -lastName
 *     -parentUser
 *         -User
 *             -username
 *             -person
 *                 -Person
 *                     -firstName
 *                     -lastName
 *                     -parentUser
 *                         -User
 *                             -username
 *                             -person
 *                                 -We stop after a allowed depth level
 *                             -emails
 *                                 -Email
 *                                     -email
 *             -emails
 *                 -Email
 *                     -email
 * It can be observed that that User.emails is expanded to the propper type. This can be achieved
 * by sending a parameter to the constructor that forces expanding generic collections to the propper type.
 * @author liviug
 */
public class XTree {
	private List<XTreeElement> _rootElements = new ArrayList<XTreeElement>();
	private TypedXmlSchema _schema;
	private int _cyclicDepthLevel;
	/**
	 * Holds a hash with classes, in order to detect cyclic paths. 
	 */
	private Map<TSClass, Integer> _cyclicDepths = new HashMap<TSClass, Integer>();
	private boolean _expandCollectionsSoda; 
	
	/**
	 * Constructs an XTree.
	 * @param schema The XmlSchema used to construct the XTree for.
	 * @param cyclicDepthLevel If there are cyclic references between different classes, this
	 * specifies how many instances of the same class may occur on any given path (starting 
	 * from the root and going to the leaf). This must be at least 1.
	 * @param expandCollectionsSoda When this is true, any generic collection will be expanded
	 * into the generic argument that SODA uses for queries. For example 'Map<User,  Pilot>'
	 * will be expanded to a 'User' type.
	 * @throws OdbgenException
	 */
	public XTree(TypedXmlSchema schema, int cyclicDepthLevel, boolean expandCollectionsSoda) throws OdbgenException{
		
		//Sanity check.
		if(cyclicDepthLevel<1){
			throw new OdbgenException("CyclicDepthLevel must be greater than 0.");
		}
		
		this._schema = schema;
		this._cyclicDepthLevel = cyclicDepthLevel;
		this._expandCollectionsSoda = expandCollectionsSoda;
		this.buildXTree();
	}

	/**
	 * Returns the list of root elements contained by this XTree.
	 * @return
	 */
	public List<XTreeElement> getRootElements() {
		return _rootElements;
	}
	
	/**
	 * Builds the XTree so that it can be queried later.
	 * This is called automatically from the constructor.
	 * @throws OdbgenException
	 */
	private void buildXTree() throws OdbgenException{
		//Iterate each class and build the tree for it.
		for(TSClass tsClass : this._schema.getClasses()){
			//Process only classes that are part of XmlSchema.
			if(tsClass.getElement()!=null){
//TODO: remove this
//if(tsClass.getSimpleName().equals("Car")){
//	System.out.println("");
//}
				XTEClass rootElem = this.buildTreeForClass(tsClass);
				this._rootElements.add(rootElem);
			}
		}
	}

	/**
	 * Recursive method that builds the XTree.
	 * @param tsClass The coot class to generate the XTree for.
	 * @return Returns the XTree corresponding to the class.
	 * @throws OdbgenException
	 */
	private XTEClass buildTreeForClass(TSClass tsClass) throws OdbgenException {
//TODO: remove this
//if(tsClass.getSimpleName().equals("SensorReadout")){
//	System.out.println("");
//}
		
		
		//Add the class to the cyclic depth hash table in order to detect cyclic paths.
		this.addClassToCyclicDepth(tsClass);
		
		XTEClass classElem = new XTEClass();
		classElem.setTsClass(tsClass);
		
		//Iterate each field and add them to classElem.
		for(TSField classField : tsClass.getFields()){
			//Create a copy of the class field because we may need to modify it.
			TSField field = classField.copy();
			XTEField fieldElem = new XTEField();
			fieldElem.setField(field);
			fieldElem.setParent(classElem);

			//Decide whether the type declaration is a collection that can be expanded for
			//SODA usage.
			if(this._expandCollectionsSoda){
				DataType colExpansion = field.getType().getGenericCollectionSodaArgument();
				if(colExpansion!=null){
					//This means this field is a collection. We will expand it to it's generic
					//type by replacing the collection with it's generic type. For example
					//if this field has type 'List<Email>', we will replace its type with 'Email'. 
					//It is possible change the type of the field because this is a copy of the
					//XmlSchema field.
					field.setType(colExpansion);
				}
			}
			
			
			//Decide whether the type of the field represent a class in our schema.
			TypeDeclaration typeDeclaration;
			try{
				typeDeclaration = this._schema.resolveType(field.getType().getQualifiedName());
			}
			catch(OdbgenTypeNotFoundException e){
				typeDeclaration = null;
			}
			catch(OdbgenTypeConflictException e){
				throw new OdbgenException(e, "Type conflict: '%s'.", field.getType().getQualifiedName());
			}
			
			
			//If we found a typedeclaration we have to add it to the tree.
			if(typeDeclaration !=null){
				if (typeDeclaration instanceof TSClass) {
					TSClass childClass = (TSClass) typeDeclaration;

					//Detect any chyclic paths.
					//Add this class only if the maximum depth is not exceeded.
					if(this.getCyclicDepthNumber(childClass)<=this._cyclicDepthLevel){
						XTEClass childClassElem = this.buildTreeForClass(childClass);
						fieldElem.getChildren().add(childClassElem);
						childClassElem.setParent(fieldElem);
					}
				}
				else if(typeDeclaration instanceof TSEnum) {
					TSEnum childEnum = (TSEnum) typeDeclaration;
					XTEEnum enumElem = new XTEEnum();
					enumElem.setEnum(childEnum);
					enumElem.setParent(fieldElem);
					fieldElem.getChildren().add(enumElem);
				}
			}
			classElem.getChildren().add(fieldElem);
		}
		
		//Decrement the counter of this class inside the cyclic depth hash table.
		this.removeClassToCyclicDepth(tsClass);
		return classElem;
	}

	private void removeClassToCyclicDepth(TSClass tsClass) throws OdbgenException {
		if(this._cyclicDepths.containsKey(tsClass)){
			int counter = this._cyclicDepths.get(tsClass);
			counter--;
			this._cyclicDepths.remove(tsClass);
			this._cyclicDepths.put(tsClass, counter);
		}
		else{
			throw new OdbgenException("Class '%s' was not found inside cyclick depth hash.", tsClass.getQualifiedName());
		}
	}

	private int getCyclicDepthNumber(TSClass tsClass) {
		if(this._cyclicDepths.containsKey(tsClass)){
			return this._cyclicDepths.get(tsClass);
		}
		else{
			return 0;
		}
	}

	private void addClassToCyclicDepth(TSClass tsClass) {
		if(!this._cyclicDepths.containsKey(tsClass)){
			this._cyclicDepths.put(tsClass, 0);
		}
		else{
			int counter = this._cyclicDepths.get(tsClass);
			counter++;
			this._cyclicDepths.remove(tsClass);
			this._cyclicDepths.put(tsClass, counter);
		}
	}

	
}











